<?php
/** ================================
 *  Intellect Board 3
 *  http://intblite.ru
 *  Абстрактный (условно) класс, от которого будут наследоваться классы типовых разделов: форума, категории, темы и т.п.
 *
 *  ================================ */

class UniForum extends Module {

  /** Выполнение запрошенного действия. От такой же процедуры в Module отличается наличием
   * проверки прав доступа на просмотр форума и его чтение.
   *
   * @param string $action Название действия (для его выполнения в классе должна существовать процедура с именем action_<название_действия>).
   * @param string $params Параметры для выполнения действия (обычно номер страницы).
   */
  function process_action($action,$params=false) {
    global $app;
    $read=$app->user->check_access('read');
    $view=$app->user->check_access('view');
    if (is_numeric($action)) { $params=$action; $action='view'; }
    if ($read && $view && $params!=1) return parent::process_action($action,$params); // если проверка прошла успешно, вызываем родительскую процедуру
    elseif (!$view) { // если нет прав на просмотр раздела, генерируем 404
      $app->status=403;
      $app->mode=1;
      $app->message('Страница с заданным URL не найдена!',3);
    }
    elseif (!$read) { // если нет прав на чтение, но есть права на просмотр, генерируем 403
      $app->status=404;
      $app->mode=1;
      $app->message('У вас нет прав на просмотр данного раздела!',3);
    }
    else { // если
      $app->status=301;
      $app->location=$app->hurl($this->data['url']).'/'; // если запросили первую страницу, то переходим на адрес без 1.htm
    }
    return false;
  }

  /** Получение настройки рекурсивно: сначала из настроек пользователя, если там настройки нет, то из настроек текущего раздела,
   * далее сайта в целом, и если все настройки отсутствуют, то подстановка значения по умолчанию.
   *
   * @global Application $app
   * @param string $name Название настройки, которую требуется получить.
   * @param mixed $default Значение настройки по умолчанию.
   * @return mixed
   */
  function get_opt_recursive($name,$default) {
    global $app;
    $result=$app->get_opt($name,'user'); // проверка настройки у пользователя
    if (!$result && isset($this->data['id'])) $result=$app->get_opt($name,'object',$this->data['id']); // проверка настройки для текущего объекта
    if (!$result) $result=$app->get_opt($name); // проверка настройки для сайта в целом
    if (!$result) $result=$default; // если ничего не задано, берем значение по умолчанию
    return $result;
  }

  /** Получение указанного количества сообщений из темы с их обработкой.
   *
   * @global Application $app
   * @param int $oid Идентификатор объекта (темы)
   * @param int $offset Смещение от начала темы (в сообщениях)
   * @param int $count Коичество сообщений, которые надо вывести
   * @param int $direction Порядок вывода (false -- прямой, true -- обратный)
   * @param string $status Статус сообщений (0 -- нормальное, 1 -- на премодерации, 2 -- удаленное)
   * @param int $mode Режим обработки сообщений: 0 -- для вывода, 1 -- для редактирования
   * @return array
   */
  function get_posts($offset,$count,$direction=0,$status="0",$mode=0) {
    global $app;

    $sql = 'SELECT p.id, p.uid, author, postdate, editdate, editcount, value, locked, html, bcode, smiles, links, '.
            'text1, text2, uc.data '.
            'FROM '.DB_prefix.'post p '.
            'LEFT JOIN '.DB_prefix.'post_text pt ON (p.id=pt.pid AND pt.active="1") '.
            'LEFT JOIN '.DB_prefix.'user_cache uc ON (p.uid=uc.uid) '.
            'WHERE p.oid='.intval($this->data['id']).' AND p.status="'.$app->slashes($status).'" '.
            'ORDER BY p.postdate';
    if ($direction) $sql.=' DESC'; // если вывод в обратном порядке
    $posts = $app->db->select_all($sql, $offset, $count);

    for ($i=0, $count=count($posts); $i<$count; $i++) { // дополнительная обработка сообщений
      $posts[$i]=$this->process_post($posts[$i],$mode);
    }

    return $posts;
  }

  /** Получение отдельного сообщения по идентификатору.
   *
   * @global Application $app
   * @param int $pid Идентификатор сообщения
   * @param boolean $process Необходимость предварительной обработки сообщения (HTML, BoardCode, смайлики и т.п.)
   * @return array Массив с данными сообщения
   */
  function get_post_by_id($pid,$mode=0) {
    global $app;

    $sql = 'SELECT p.id, oid, uid, author, postdate, editdate, editcount, value, locked, html, bcode, smiles, links, '.
            'text1, text2 '.
            'FROM '.DB_prefix.'post p '.
            'LEFT JOIN '.DB_prefix.'post_text pt ON (p.id=pt.pid AND pt.active="1") '.
            'WHERE p.id='.intval($pid);
    $post=$app->db->select_row($sql);

    $post=$this->process_post($post,$mode);

    return $post;
  }

/** Обработка сообщения перед выводом
 *
 * @param array $post Необработанное сообщение
 * @param int $mode Режим обработки: 0 -- для вывода, 1 -- для редактирования
 * @return array Обработанное сообщение
 */
  function process_post($post,$mode=0) {
    if ($mode==0) {
    if (isset($post['data']) && ($post['data'])) $post['userdata']=unserialize($post['data']); // извлекаем кешированные данные о пользователе

    if ($post['html']==0) $post['text1']=$this->post_nohtml($post['text1']); // если использование HTML в комментариях запрещено, вызываем процедуру удаления тегов
    $post['text2']=$this->post_nohtml($post['text2']); // в комментариях модераторов HTML запрещен всегда

    if ($post['bcode']) $post['text1']=$this->post_bcode($post['text1']);
    $post['text2']=$this->post_bcode($post['text2']); // в комментарии модератора BoardCode включен всегда

    if ($post['smiles']) {
      $post['text1']=$this->post_smiles($post['text1']);
      $post['text2']=$this->post_smiles($post['text2']);
    }

    if ($post['links']) {
      $post['text1']=$this->post_links($post['text1']);
      $post['text2']=$this->post_links($post['text2']);
    }
    }
    else {
      $post['text1']=htmlspecialchars($post['text1']);
      if (isset($post['text2'])) $post['text2']=htmlspecialchars($post['text2']);
    }
    return $post;
  }

  /** Обработка текста, предназначенного для вывода без использования HTML: замена тегов на последовательности и \n на <br />
   *
   * @param string $text Текст для обработки
   * @return string Обработанный текст
   */
  function post_nohtml($text) {
    return str_replace("\n","<br />\n",(htmlspecialchars($text)));
  }

  /** Обработка тегов BoardCode в тексте
   *
   * @param string $text Текст для обработки
   * @param int $mode Режим обработки (0 -- обычный, 1 -- для подписи без img, 2 -- для подписи с img)
   * @return string Обработанный текст
   */
  function post_bcode($text,$mode=0) {
    $text = preg_replace('|\[b\](.*?)\[/b\]|s','<b>$1</b>',$text);
    $text = preg_replace('|\[i\](.*?)\[/i\]|s','<i>$1</i>',$text);
    $text = preg_replace('|\[u\](.*?)\[/u\]|s','<u>$1</u>',$text);
    $text = preg_replace('|\[s\](.*?)\[/s\]|s','<s>$1</s>',$text);

    // TODO: доделать другие теги
    // TODO: сделать задаваемые теги, которые будут храниться в обычном или нумерованном списке
    return $text;
  }

   /** Обработка графических смайликов
   *
   * @param string $text Текст для обработки
   * @return string Обработанный текст
   */
  function post_smiles($text) {
    // TODO: сделать обработку смайликов, возможно, хранить их в нумерованном списке
    return $text;
  }

   /** Обработка неактивных гиперссылок
   *
   * @param string $text Текст для обработки
   * @return string Обработанный текст
   */
  function post_links($text) {
    // TODO: сделать обработку ссылок, начинающихся с http://, https://, ftp://, www. или кончающихся на .com, .net, .org, .ru, .su или .рф., не выделенных тегами
    $text = preg_replace('|(http://[^\s"]+)|','<a href="$1">$1</a>',$text);
    return $text;
  }
  

  function get_post_offset($tid,$time,$direction=false) {
    global $app;

    $sql = 'SELECT COUNT(id) FROM '.DB_prefix.'post WHERE oid='.intval($tid);
    if (!$direction) $sql.=' AND postdate<'.intval($time);
    else $sql.=' AND postdate>'.intval($time);
    return $app->db->select_int($sql);
  }

  /** Получение ссылки на страницу, на которой содержится сообщение с указанным от начала темы смещением
   *
   * @global Application $app
   * @param integer $offset Смещение сообщения
   * @param boolean $direction Направление сортировки сообщений в теме (от старых к новым или наоборот)
   */
  function get_page_link($offset,$direction) {
    global $app;

    $perpage = $this->get_opt_recursive('topic_perpage',20); // количество сообщений на странице
    if (!$direction) $page = floor($offset/$perpage)+1;
    else $page = floor(($this->data['posts']-$offset)/$perpage)+1;
    if ($page==1) $page=''; // для первой страницы номер не ставим
    else $page=$page.'.htm';
    $result = $app->hurl($this->data['url']).'/'.$page;
    return $result;
  }

  /** Фиксация факта просмотра темы. Для зарегистрированных пользователей фиксируется время просмотра, для всех -- увеличивается кол-во просмотров на единицу. **/
  function fix_view() {
    global $app;
    $time=time();
    $lastvisit = false;
    $timeout = $app->get_opt('visit_time')*60; // время, которое должно пройти после визита в тему, после которого она будет считаться новой.
    if (!$timeout) $timeout=15*60;
    if (!$app->user->is_guest()) { // время последнего визита фиксируется только для зарегистрированных пользователей
      $sql = 'SELECT visit1, visit2, bookmark, subscribe '.
              'FROM '.DB_prefix.'object_visit '.
              'WHERE oid='.intval($this->data['id']).' AND uid='.$app->user->get_uid();
      $lastvisit = $app->db->select_row($sql);
      if (empty($lastvisit)) { // если предыдущего посещения темы не было (зашли впервые)
        $lastvisit['visit1']=0;
        $lastvisit['visit2']=$time;
        $lastvisit['oid']=$this->data['id'];
        $lastvisit['uid']=$app->user->get_uid();
        $app->db->insert(DB_prefix.'object_visit', $lastvisit);
      }
      else {
        if ($lastvisit['visit1']<$time-$timeout) { // если предыдущий визит был раньше, чем указанное время таймаута
          $lastvisit['visit1']=$lastvisit['visit2'];  // если да, то переносим последнее время текущего визита (которое на самом деле оказывается последним времением предыдущего визита) в предыдущий
        }
        $lastvisit['visit2']=$time; // и в любом случае фиксируем время текущего визита
        $app->db->update(DB_prefix.'object_visit', $lastvisit, 'oid='.intval($this->data['id']).' AND uid='.$app->user->get_uid());
      }
    }

    // TODO: добавить проверку, что пользователь -- не поисковый бот. Возможно, сделать еще один столбец для фиксации суточных просмотров.
    $sql = 'UPDATE '.DB_prefix.'object_views SET views=views+1 WHERE oid='.intval($this->data['id']);
    $app->db->query($sql);
    if ($app->db->affected_rows()==0) {
      $sql='INSERT INTO '.DB_prefix.'object_views SET views=1, oid='.intval($this->data['id']);
      $app->db->query($sql);
    }
    return $lastvisit;
  }

  /** Сохранение сообщения в базу, а также его поисковая индексация
   *
   * @global Application $app
   * @param array $postdata Данные о сообщении (для таблицы post)
   * @param boolean $overwrite Нужно ли удалять старые версии сообщения. (Для Wiki-раздела и статики могут требоваться старые версии сообщений. )
   */
  function save_post($postdata,$overwrite=true) {
    global $app;
    $time=time();
    $textdata['text1']=$postdata['text1'];
    $textdata['text2']=isset($postdata['text2']) ? $postdata['text2'] : '';
    $textdata['active']=1;
    unset($postdata['text1']); // TODO: заменить потом на фильтрацию
    unset($postdata['text2']);
    $postdata['oid']=$this->data['id']; // номер объекта, которому принадлежит сообщение

    $savedata=$app->filter($postdata,array('id','oid','uid','author','postdate','editdate','editcount','value','status','locked','html','bcode','smiles','links','typograf','ip')); // все необходимые проверки к моменту сохранения уже должны быть сделаны

    if (isset($postdata['id']) && $postdata['id']) { // если у сообщения есть идентификатор, то оно не новое, редактируем существующее

      $app->db->update(DB_prefix.'post',$savedata,'id='.intval($postdata['id']));
      if ($overwrite) { // если старые версии
        $sql = 'DELETE FROM '.DB_prefix.'post_text WHERE pid='.intval($postdata['id']);
        $app->db->query($sql);
      }
      else {
        $sql = 'UPDATE '.DB_prefix.'post_text SET active="0" WHERE pid='.intval($postdata['id']);
        $app->db->query($sql);
      }
    }
    else $postdata['id']=$app->db->insert(DB_prefix.'post',$savedata);
    $textdata['pid']=$postdata['id'];
    $app->db->insert(DB_prefix.'post_text',$textdata);
    // TODO: сделать поисковую индексацию сообщения
  }

  /** Прием данных сообщения и установка недостающих данных по умолчанию.
   *
   * @global Application $app
   * @param array $data Исходные данные сообщения
   * @param int $mode Режим приема данных: 0 -- обычный (без фильтрации), 1 -- из формы, отправленной с помощью POST
   * @return array Массив с полными данными сообщения
   */
  function set_post($data=false,$mode=0) {
    global $app;
    $time=time();

    if ($mode==0) { // если режим установки данных -- без доп. фильтрации, потом выставляем значения по умолчанию
      if (!empty($data)) $result=$data;
      if (!isset($result['html'])) $result['html']=0;
      if (!isset($result['bcode'])) $result['bcode']=1;
      if (!isset($result['smiles'])) $result['smiles']=1;
      if (!isset($result['links'])) $result['links']=1;
    }
    elseif ($mode==1) {
      $result=$app->filter($data,array('id','text1','html','bcode','smiles','links')); // фильтруем то, что пришло из формы, чтобы пользователь не вбросил чего лишнего
      // если из формы не пришли
      if (!isset($result['html'])) $result['html']=0;
      if (!isset($result['bcode'])) $result['bcode']=0;
      if (!isset($result['smiles'])) $result['smiles']=0;
      if (!isset($result['links'])) $result['links']=0;
    }
    $result['editdate']=$time; // если вдруг потребется, чтобы время последней модификации не равнялось текущему, нужно будет делать это из модуля
    if (!isset($result['postdate'])) $result['postdate']=$time;
    $result['locked']=0;
    $result['ip']=$app->get_ip();
    if (!isset($result['oid']) && isset($this->data['id'])) $result['oid']=$this->data['id']; // аналогично с темой, которой принадлежит это сообщение
    if (!isset($result['uid'])) {
      $result['uid']=$app->user->get_uid();
      $result['author']=$app->user->get_name(); // TODO: потом сделать так, чтобы имя вставлялось только в случае гостя
    }
    if (!isset($result['allow_htmk'])) $result['allow_html']=$app->user->check_access('html');
    if (!isset($result['html'])) $result['html']=$this->get_opt_recursive('use_html', 0);
    if (!isset($result['bcode'])) $result['bcode']=$this->get_opt_recursive('use_bcode', 1);
    if (!isset($result['smiles'])) $result['smiles']=$this->get_opt_recursive('use_smiles', 1);
    if (!isset($result['links'])) $result['links']=$this->get_opt_recursive('use_links', 1);
    if (!isset($result['text1'])) $result['text1']='';

    // для locked и text2 не устанавливаем ничего
    return $result;
  }

  /** Проверка корректности отправляемого сообщения.
   *
   * @global <type> $app
   * @param array $postdata Массив с данными сообщения
   * @param boolean $rights Нужно ли проверять права на отправку сообщения (post) и таймаут.
   */
  function validate_post(&$postdata,$rights=true) {
    global $app;
    $result=array();
    if (!$postdata['text1']) $result[]=array('text'=>'Сообщение не может быть пустым!','level'=>3);
    if ($this->data['locked']) $result[]=array('text'=>'Тема закрыта! Отправка новых сообщений запрещена.','level'=>3); //TODO: проверка закрытости родительских разделов и модераторских прав
    $maxlen=$app->get_opt('post_maxlen');
    if (!$maxlen) $maxlen=24000;
    if (strlen($postdata['text1'])>$maxlen) $result[]=array('text'=>'Длина сообщения превышает максимально допустимую ('.$maxlen.' символов)!','level'=>3);

    if (!$app->user->check_access('html')) $postdata['html']=0; // если HTML не разрешен, сбрасываем признак его использования

    if ($rights) { // если запрошена проверка прав
      if (!$app->user->check_access('post')) {
        $result[]=array('text'=>'У вас нет прав на отправку сообщения в эту тему!','level'=>3);
      }
      // контроль флуд-интервала
      // TODO: брать время флуд-контроля из настроек группы пользователя
      if (empty($result) && $app->load_module('antibot', false)) { // проверку по таймауту имеет смысл делать только в случае, если не возникло ошибок, иначе пользователю придется каждый раз ждать после исправления ошибки
        $antibot = new antibot;
        $delay = $app->get_opt('post_floodtime');
        if (!$delay) $delay=30;
        if (!$antibot->timeout_check('post_floodtime', $delay)) $result[]=array('text'=>'С момента отправки предыдущего сообщения прошло менее '.$delay.' секунд!','level'=>3);
      }
    }
    return $result;
  }

  /** Проверка корректности создаваемого объекта
   *
   * @global Application $app
   * @param boolean $rights Нужно ли проверять права на отправку сообщения (post) и таймаут.
   */
  function validate_object($rights=true) {
    global $app;
    $result=array();
    if (!$this->data['title']) $result[]=array('text'=>'Заголовок не может быть пустым!','level'=>3);
    if ($this->data['locked']) $result[]=array('text'=>'Раздел закрыт! Отправка новых сообщений запрещена.','level'=>3); //TODO: проверка закрытости родительских разделов и модераторских прав
    if ($this->data['newurl'] && !preg_match('/^\w[\w\d]*+$/',$this->data['newurl'])) $result[]=array('text'=>'Ошибка: HURL содержит некорректные символы!','level'=>3); // URL не может начинаться с цифры, чтобы не было конфликта с автоматически сгенерированными URL

    if ($rights) { // если запрошена проверка прав
      if (!$app->user->check_access('newobject')) {
        $result[]=array('text'=>'У вас нет прав на создание новых тем!','level'=>3);
      }
      // контроль флуд-интервала
      // TODO: брать время флуд-контроля из настроек группы пользователя
      if (empty($result) && $app->load_module('antibot', false)) {
        $antibot = new antibot;
        $delay = $app->get_opt('object_floodtime') or 60;
        if (!$antibot->timeout_check('object_floodtime', $delay)) $result[]=array('text'=>'С момента создания предыдущей темы прошло менее '.$maxlen.' секунд!','level'=>3);
      }
    }
    return $result;
  }

  /** Обновление родительских объектов (время последнего изменения, кол-во тем и соообщений)
   *
   * @param array $oids Массив идентификаторов объектов или самих объектов
   * @param int $lastmod Время последнего изменения объектов
   * @param int $obj_inc Увеличение счетчика тем
   * @param int $post_inc Увеличение счетчика сообщений
   */
  function update_objects($objects,$lastmod,$post_inc=0,$obj_inc=0) {
    global $app;
    if (is_object($objects[0])) { // если переданы сами объекты, а не их номера
      $oids=array();
      for ($i=0, $count=count($objects); $i<$count; $i++) $oids[]=$objects[$i]->data['id'];
    }
    elseif (is_array($objects[0])) { // если переданы сами объекты, а не их номера
      $oids=array();
      for ($i=0, $count=count($objects); $i<$count; $i++) $oids[]=$objects[$i]['id'];
    }
    else $oids=$objects;
    $sql ='UPDATE '.DB_prefix.'object SET lastmod='.intval($lastmod).', subitems=subitems+'.intval($obj_inc).', posts=posts+'.intval($post_inc).
          ' WHERE '.$app->db->array_to_sql($oids, 'id');
    return $app->db->query($sql);
  }

  /** Отметка сообщений как удаленных (простановка status=1 и коррекция времени модификации и числа сообщений)
   *
   * @global Application $app
   * @param array $pids Массив идентификаторов сообщений для удаления (или единичный номер сообщения)
   * @return int Количество видимых сообщений, которые были помечены для удаления
   */
  function delete_posts($pids) {
    global $app;
    if (!is_array($pids)) $pids=array(intval($pids));

    $sql = 'UPDATE '.DB_prefix.'post SET status=1 WHERE status=0 oid='.intval($this->data['id']).' AND '.$app->db->array_to_sql($pids, 'id');
    $app->db->query($sql);

    $deleted = $app->db->affected_rows();
    
    if ($deleted) { // если что-то действительно удалили, проводим коррекцию статистики
      $time = time();
      $this->update_objects($app->objparents, $time, -$deleted, 0);
    }

    // отмечаем сообщения, стоящие на премодерации, для них никаких коррекций в статистике не надо
    $sql = 'UPDATE '.DB_prefix.'post SET status=1 WHERE status=2 oid='.intval($this->data['id']).' AND '.$app->db->array_to_sql($pids, 'id');
    $app->db->query($sql);

    if ($this->data['posts']==$deleted) { // если удалили столько сообщений, сколько было видимых сообщений в теме, то саму тему тоже делаем удаленной
      $this->data['status']=1; // признак того, что тема удалена
      $this->data['posts']=0;
      $this->save_object(false);
    }
    else $this->data['posts']=$this->data['posts']-$deleted;

    return $deleted;
  }

  /** Окончательное удаление сообщений, помеченных для удаления (status=2) из базы
   *
   * @global Application $app
   * @param array $oids Массив идентификаторов объектов, для которых требуется провести удаление
   */
  function clear_posts($oids) {
    global $app;
    if (!is_array($oids)) $oids=array(intval($oids));

    $sql = 'SELECT id FROM '.DB_prefix.'post WHERE status=2 AND '.$app->db->array_to_sql($oids, 'oid');
    $pids = $app->db->select_all_numbers($sql);

    // TODO: сделать удаление прикрепленных файлов
    $sql = 'DELETE FROM '.DB_prefix.'post WHERE '.$app->db->array_to_sql($pids, 'id'); // удаляем данные о сообщениях
    $app->db->query($sql);

    $sql = 'DELETE FROM '.DB_prefix.'post_text WHERE '.$app->db->array_to_sql($pids, 'pid'); // удаляем сами сообщения
    $app->db->query($sql);
  }

}

?>
